Pular para o conteúdo principal

Impressão

A impressão é acessível por meio de client.printer, disponível após a conexão com o serviço BTG Pay descrita na Configuração. As operações de impressão são assíncronas (suspend) e retornam Result<Unit> — nenhuma exceção escapa do método.

O conceito de job​

A impressão funciona por meio de jobs. Dentro do bloco print { }, os elementos são declarados na ordem em que serão impressos no papel. Cada elemento — texto, imagem, QR code ou avanço de papel — é adicionado sequencialmente, e o SDK os envia à impressora nessa mesma ordem.

import btgpay.client.printer.Align

lifecycleScope.launch {
client.printer.print {
text("OLA MUNDO", size = 28, align = Align.CENTER, bold = true)
feed(lines = 4)
}.onSuccess {
Log.i(TAG, "impresso")
}.onFailure { e ->
Log.e(TAG, "falhou: ${e.message}")
}
}

Imprimir texto​

O método text adiciona uma linha de texto ao job. O único parâmetro obrigatório é o conteúdo; os demais possuem valores padrão.

lifecycleScope.launch {
client.printer.print {
text("MERCADO SILVA", size = 28, align = Align.CENTER, bold = true)
text("CNPJ 30.306.294/0001-45", size = 14, align = Align.CENTER)
text("")
text("Rua das Flores, 123")
text("Sao Paulo - SP")
text("Obrigado pela preferencia!", align = Align.CENTER)
feed(lines = 4)
}
}
ParâmetroTipoDefaultDescrição
textStringobrigatórioConteúdo do texto. Máximo 4.096 caracteres.
sizeInt16Corpo da fonte.
alignAlignAlign.LEFTLEFT, CENTER ou RIGHT.
boldBooleanfalseNegrito.
marginLeftInt0Margem esquerda, em pixels.
marginRightInt0Margem direita, em pixels.
lineSpaceInt0Espaço extra entre linhas.

A fonte utilizada é a do firmware da impressora — não há como escolher a família tipográfica por esse método. A quebra de linha também é controlada pelo firmware: uma linha maior que a largura do papel é quebrada automaticamente. O negrito é feito pela própria impressora, engrossando o traço, e não por uma segunda fonte.

Para controle total sobre tipografia, as alternativas são a impressão por imagem ou o Template de Comprovante.

Imprimir imagem​

O método image aceita exclusivamente PNG. O papel tem 384 pixels de largura, e o SDK não reescala a imagem — ela deve ser gerada já na largura correta.

lifecycleScope.launch {
val logo: ByteArray = assets.open("logo.png").use { it.readBytes() }

client.printer.print {
image(logo, align = Align.CENTER)
text("MERCADO SILVA", size = 22, align = Align.CENTER, bold = true)
feed(lines = 4)
}
}
ParâmetroTipoDefaultDescrição
pngByteArrayobrigatórioBytes de um PNG. Máximo 512 KB.
alignAlignAlign.CENTERLEFT, CENTER ou RIGHT.
marginLeftInt0Margem esquerda, em pixels.
marginRightInt0Margem direita, em pixels.

Para gerar o PNG a partir de um Bitmap Android, o seguinte helper reescala a imagem para a largura do papel:

import android.graphics.Bitmap
import java.io.ByteArrayOutputStream

fun Bitmap.toPrinterPng(): ByteArray {
val alvo = 384
val escalado = if (width != alvo) {
Bitmap.createScaledBitmap(this, alvo, height * alvo / width, true)
} else {
this
}
return ByteArrayOutputStream().use { out ->
escalado.compress(Bitmap.CompressFormat.PNG, 100, out)
out.toByteArray()
}
}

Logo e arte vetorial ficam bem abaixo do limite de 512 KB. Conteúdo fotográfico, no entanto, pode ultrapassá-lo: um PNG de 384x1200 com conteúdo fotográfico passa de 1,3 MB. A recomendação é converter para escala de cinza ou reduzir a altura antes de enviar. Se o limite for estourado, o erro é explícito (image-too-large) e informa o índice do elemento.

A impressora é térmica e monocromática. A conversão de meio-tom é feita pelo driver da impressora, e preto sólido produz resultados mais previsíveis que gradiente.

Imprimir QR code​

O QR code é rasterizado pelo SDK, não pelo firmware da impressora.

lifecycleScope.launch {
client.printer.print {
text("Consulte sua nota fiscal", align = Align.CENTER)
qr("https://nf.e/abc123")
feed(lines = 4)
}
}
ParâmetroTipoDefaultDescrição
contentStringobrigatórioConteúdo codificado no QR.
alignAlignAlign.CENTERLEFT, CENTER ou RIGHT.
sizeInt240Lado do QR, em pixels, no papel de 384 px.

O parâmetro size merece atenção. Conteúdo mais longo exige mais módulos no QR, e cada módulo precisa de ao menos um pixel de lado. Um size que funciona para uma URL curta pode ser recusado para uma URL longa, porque os módulos ficariam menores que um pixel e o código sairia ilegível. O erro nesse caso é explícito (qr-render-failed) — o SDK nunca gera um código em branco. Para URLs de tamanho variável, usar size = 280 ou tratar a falha é uma abordagem segura.

Avançar o papel​

O método feed avança o papel em linhas.

lifecycleScope.launch {
client.printer.print {
text("CUPOM")
feed(lines = 4)
}
}

Sem feed no final do job, a última linha impressa fica presa dentro do mecanismo da impressora e o operador não consegue destacar o cupom. Quatro linhas é um valor prático para esse espaço.

Reutilizar um job​

A função printJob { } constrói um PrintJob imutável, que pode ser impresso quantas vezes forem necessárias. Essa abordagem é útil para emitir segunda via.

import btgpay.client.printer.printJob
import btgpay.client.printer.PrintJob

val comprovante = printJob {
text("MERCADO SILVA", size = 28, align = Align.CENTER, bold = true)
text("VIA DO CLIENTE", align = Align.CENTER)
qr("https://nf.e/abc123")
feed(lines = 4)
}

lifecycleScope.launch {
client.printer.print(comprovante) // via do cliente
client.printer.print(comprovante) // via do estabelecimento
}

PrintJob é um data class contendo a lista de elementos, o que permite inspecionar ou serializar o job antes de imprimi-lo.

Estado da impressora​

O método status() consulta o estado atual da impressora e retorna um PrinterStatus.

import btgpay.client.printer.PrinterStatus

lifecycleScope.launch {
when (client.printer.status()) {
PrinterStatus.Ready -> imprimir()
PrinterStatus.NoPaper -> avisar("Coloque papel")
PrinterStatus.Overheated -> avisar("Impressora quente, aguarde")
PrinterStatus.Unavailable -> avisar("Impressora indisponível")
}
}

PrinterStatus é uma sealed interface, o que torna o when exaustivo — o compilador avisa se um estado novo for adicionado em versões futuras.

Consultar o estado antes de imprimir é opcional: o print já falha com o erro adequado se houver algum problema. A consulta é útil quando se deseja avisar o operador antes de iniciar um job longo.

Tratamento de erros​

Nenhuma chamada de impressão lança exceção. O resultado vem em Result, e a falha é sempre uma PrintException.

import btgpay.client.printer.PrintException

lifecycleScope.launch {
client.printer.print { /* ... */ }.onFailure { e ->
val erro = e as PrintException
Log.e(TAG, "brn=${erro.brn} indice=${erro.failedElementIndex} msg=${erro.message}")
}
}

Os campos de PrintException são:

CampoTipoDescrição
brnStringCódigo estável do erro, ex. brn:btg:pay:hal:printer:out-of-paper.
severityStringGravidade do erro.
messageStringMensagem legível.
detailsString?Contexto adicional, quando houver.
failedElementIndexInt?Índice do elemento que falhou, ou null.

Atomicidade​

Um job não é atômico. Se o papel acaba no terceiro elemento de cinco, os dois primeiros já estão impressos e não há como desfazê-los. O failedElementIndex indica exatamente onde o job parou, permitindo retomar a impressão a partir daquele ponto.

O exemplo abaixo demonstra uma estratégia de retomada. A cada tentativa, os elementos já impressos são descartados com base no failedElementIndex. Se o índice não estiver disponível (erro não vinculado a um elemento específico), a função encerra sem retentar.

suspend fun imprimirComRetomada(job: PrintJob) {
var elementosImpressos = 0
repeat(3) {
val restante = PrintJob(job.elements.drop(elementosImpressos))
val resultado = client.printer.print(restante)
if (resultado.isSuccess) return
val falha = resultado.exceptionOrNull() as? PrintException
val indice = falha?.failedElementIndex ?: return
elementosImpressos += indice
}
}

Códigos de erro​

Hardware e estado da impressora:

brnSignificado
...:printer:out-of-paperSem papel.
...:printer:overheatingCabeça superaquecida.
...:printer:hardware-failureFalha de hardware.
...:printer:printer-timeoutA impressora não respondeu.

Job recusado antes de imprimir:

brnSignificado
...:printer:empty-jobNenhum elemento no job.
...:printer:too-many-elementsAcima de 32 elementos.
...:printer:image-too-largeImagem acima de 512 KB.
...:printer:text-too-longTexto acima de 4.096 caracteres.
...:printer:payload-too-largeJob somando mais de 768 KB.
...:printer:qr-render-failedConteúdo longo demais, ou size pequeno demais.
...:printer:malformed-jobJob inválido na travessia AIDL.

Concorrência e disponibilidade:

brnSignificado
...:printer:printer-busyOutro job em andamento.
...:printer:printer-unavailableSem impressora no terminal.
...:printer:job-timed-outO job passou do tempo máximo.
...:printer:timeoutO serviço não respondeu.
...:printer:transport-failureFalha na chamada ao serviço, normalmente app desconectado.

Timeouts​

Cada chamada possui um limite de tempo, porque o transporte AIDL é assíncrono e não reporta nada se o processo do serviço morrer no meio da operação:

ChamadaLimite
print90 s
setReceiptTemplate15 s
status5 s

Um timeout chega como PrintException com brn ...:printer:timeout. Ele não significa que o papel parou de sair — apenas que a resposta do serviço não chegou.

Cancelamento​

Cancelar a coroutine não interrompe o papel que já está saindo da impressora. O cancelamento é propagado corretamente ao escopo pai (não é engolido como falha de impressão), mas o mecanismo físico da impressora não é interrompido.

Limites​

LimiteValor
Largura do papel384 px
Elementos por job32
Caracteres por texto4.096
Bytes por imagem512 KB
Bytes por job (soma)768 KB

A impressão é serial: um caminho de papel, um job por vez. Dois jobs concorrentes não se misturam — o segundo espera ou retorna com printer-busy.